<?php

namespace Luracast\Restler\Format;

use Luracast\Restler\Data\Object;
use Luracast\Restler\RestException;
use SimpleXMLElement;
use XMLWriter;

/**
 * XML Markup Format for Restler Framework
 *
 * @category Framework
 * @package Restler
 * @subpackage format
 * @author R.Arul Kumaran <arul@luracast.com>
 * @copyright 2010 Luracast
 * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @link http://luracast.com/products/restler/
 * @version 3.0.0rc5
 */
class XmlFormat extends Format {
	const MIME = 'application/xml';
	const EXTENSION = 'xml';
	
	// ==================================================================
	//
	// Properties related to reading/parsing/decoding xml
	//
	// ------------------------------------------------------------------
	public static $importSettingsFromXml = false;
	public static $parseAttributes = true;
	public static $parseNamespaces = true;
	public static $parseTextNodeAsProperty = true;
	
	// ==================================================================
	//
	// Properties related to writing/encoding xml
	//
	// ------------------------------------------------------------------
	public static $useTextNodeProperty = true;
	public static $useNamespaces = true;
	public static $cdataNames = array ();
	
	// ==================================================================
	//
	// Common Properties
	//
	// ------------------------------------------------------------------
	public static $attributeNames = array ();
	public static $textNodeName = 'text';
	public static $namespaces = array ();
	public static $namespacedProperties = array ();
	/**
	 * Default name for the root node.
	 *
	 * @var string $rootNodeName
	 */
	public static $rootName = 'response';
	public static $defaultTagName = 'item';
	
	/**
	 * When you decode an XML its structure is copied to the static vars
	 * we can use this function to echo them out and then copy paste inside
	 * our service methods
	 *
	 * @return string PHP source code to reproduce the configuration
	 */
	public static function exportCurrentSettings() {
		$s = 'XmlFormat::$rootName = "' . (self::$rootName) . "\";\n";
		$s .= 'XmlFormat::$attributeNames = ' . (var_export ( self::$attributeNames, true )) . ";\n";
		$s .= 'XmlFormat::$defaultTagName = "' . self::$defaultTagName . "\";\n";
		$s .= 'XmlFormat::$parseAttributes = ' . (self::$parseAttributes ? 'true' : 'false') . ";\n";
		$s .= 'XmlFormat::$parseNamespaces = ' . (self::$parseNamespaces ? 'true' : 'false') . ";\n";
		if (self::$parseNamespaces) {
			$s .= 'XmlFormat::$namespaces = ' . (var_export ( self::$namespaces, true )) . ";\n";
			$s .= 'XmlFormat::$namespacedProperties = ' . (var_export ( self::$namespacedProperties, true )) . ";\n";
		}
		
		return $s;
	}
	public function encode($data, $humanReadable = false) {
		$data = Object::toArray ( $data );
		$xml = new XMLWriter ();
		$xml->openMemory ();
		$xml->startDocument ( '1.0', $this->charset );
		if ($humanReadable) {
			$xml->setIndent ( true );
			$xml->setIndentString ( '    ' );
		}
		static::$useNamespaces && isset ( static::$namespacedProperties [static::$rootName] ) ? $xml->startElementNs ( static::$namespacedProperties [static::$rootName], static::$rootName, static::$namespaces [static::$namespacedProperties [static::$rootName]] ) : $xml->startElement ( static::$rootName );
		if (static::$useNamespaces) {
			foreach ( static::$namespaces as $prefix => $ns ) {
				if (isset ( static::$namespacedProperties [static::$rootName] ) && static::$namespacedProperties [static::$rootName] == $prefix)
					continue;
				$prefix = 'xmlns' . (empty ( $prefix ) ? '' : ':' . $prefix);
				$xml->writeAttribute ( $prefix, $ns );
			}
		}
		$this->write ( $xml, $data, static::$rootName );
		$xml->endElement ();
		return $xml->outputMemory ();
	}
	public function write(XMLWriter $xml, $data, $parent) {
		$text = array ();
		if (is_array ( $data )) {
			if (static::$useTextNodeProperty && isset ( $data [static::$textNodeName] )) {
				$text [] = $data [static::$textNodeName];
				unset ( $data [static::$textNodeName] );
			}
			$attributes = array_flip ( static::$attributeNames );
			// make sure we deal with attributes first
			$temp = array ();
			foreach ( $data as $key => $value ) {
				if (isset ( $attributes [$key] )) {
					$temp [$key] = $data [$key];
					unset ( $data [$key] );
				}
			}
			$data = array_merge ( $temp, $data );
			foreach ( $data as $key => $value ) {
				if (is_numeric ( $key )) {
					if (! is_array ( $value )) {
						$text [] = $value;
						continue;
					}
					$key = static::$defaultTagName;
				}
				$useNS = static::$useNamespaces && ! empty ( static::$namespacedProperties [$key] ) && false === strpos ( $key, ':' );
				if (is_array ( $value )) {
					if ($value == array_values ( $value )) {
						// numeric array, create siblings
						foreach ( $value as $v ) {
							$useNS ? $xml->startElementNs ( static::$namespacedProperties [$key], $key, null ) : $xml->startElement ( $key );
							$this->write ( $xml, $v, $key );
							$xml->endElement ();
						}
					} else {
						$useNS ? $xml->startElementNs ( static::$namespacedProperties [$key], $key, null ) : $xml->startElement ( $key );
						$this->write ( $xml, $value, $key );
						$xml->endElement ();
					}
					continue;
				} elseif (is_bool ( $value )) {
					$value = $value ? 'true' : 'false';
				}
				if (isset ( $attributes [$key] )) {
					$xml->writeAttribute ( $useNS ? static::$namespacedProperties [$key] . ':' . $key : $key, $value );
				} else {
					$useNS ? $xml->startElementNs ( static::$namespacedProperties [$key], $key, null ) : $xml->startElement ( $key );
					$this->write ( $xml, $value, $key );
					$xml->endElement ();
				}
			}
		} else {
			$text [] = ( string ) $data;
		}
		if (! empty ( $text )) {
			if (count ( $text ) == 1) {
				in_array ( $parent, static::$cdataNames ) ? $xml->writeCdata ( implode ( '', $text ) ) : $xml->text ( implode ( '', $text ) );
			} else {
				foreach ( $text as $t ) {
					$xml->writeElement ( static::$textNodeName, $t );
				}
			}
		}
	}
	public function decode($data) {
		try {
			if ($data == '') {
				return array ();
			}
			libxml_use_internal_errors ( true );
			$xml = simplexml_load_string ( $data, "SimpleXMLElement", LIBXML_NOBLANKS | LIBXML_NOCDATA | LIBXML_COMPACT );
			if (false === $xml) {
				$error = libxml_get_last_error ();
				throw new RestException ( 400, 'Malformed XML. ' . trim ( $error->message, "\r\n" ) . ' at line ' . $error->line );
			}
			libxml_clear_errors ();
			if (static::$importSettingsFromXml) {
				static::$attributeNames = array ();
				static::$namespacedProperties = array ();
				static::$namespaces = array ();
				static::$rootName = $xml->getName ();
				$namespaces = $xml->getNamespaces ();
				if (count ( $namespaces )) {
					$p = strpos ( $data, $xml->getName () );
					if ($p && $data {$p - 1} == ':') {
						$s = strpos ( $data, '<' ) + 1;
						$prefix = substr ( $data, $s, $p - $s - 1 );
						static::$namespacedProperties [static::$rootName] = $prefix;
					}
				}
			}
			$data = $this->read ( $xml );
			if (count ( $data ) == 1 && isset ( $data [static::$textNodeName] ))
				$data = $data [static::$textNodeName];
			return $data;
		} catch ( \RuntimeException $e ) {
			throw new RestException ( 400, "Error decoding request. " . $e->getMessage () );
		}
	}
	public function read(SimpleXMLElement $xml, $namespaces = null) {
		$r = array ();
		$text = ( string ) $xml;
		
		if (static::$parseAttributes) {
			$attributes = $xml->attributes ();
			foreach ( $attributes as $key => $value ) {
				if (static::$importSettingsFromXml && ! in_array ( $key, static::$attributeNames )) {
					static::$attributeNames [] = $key;
				}
				$r [$key] = static::setType ( ( string ) $value );
			}
		}
		$children = $xml->children ();
		foreach ( $children as $key => $value ) {
			if (isset ( $r [$key] )) {
				if (is_array ( $r [$key] )) {
					if ($r [$key] != array_values ( $r [$key] ))
						$r [$key] = array (
								$r [$key] 
						);
				} else {
					$r [$key] = array (
							$r [$key] 
					);
				}
				$r [$key] [] = $this->read ( $value, $namespaces );
			} else {
				$r [$key] = $this->read ( $value );
			}
		}
		
		if (static::$parseNamespaces) {
			if (is_null ( $namespaces ))
				$namespaces = $xml->getDocNamespaces ( true );
			foreach ( $namespaces as $prefix => $ns ) {
				static::$namespaces [$prefix] = $ns;
				if (static::$parseAttributes) {
					$attributes = $xml->attributes ( $ns );
					foreach ( $attributes as $key => $value ) {
						if (isset ( $r [$key] )) {
							$key = "{$prefix}:$key";
						}
						if (static::$importSettingsFromXml && ! in_array ( $key, static::$attributeNames )) {
							static::$namespacedProperties [$key] = $prefix;
							static::$attributeNames [] = $key;
						}
						$r [$key] = static::setType ( ( string ) $value );
					}
				}
				$children = $xml->children ( $ns );
				foreach ( $children as $key => $value ) {
					if (static::$importSettingsFromXml)
						static::$namespacedProperties [$key] = $prefix;
					if (isset ( $r [$key] )) {
						if (is_array ( $r [$key] )) {
							if ($r [$key] != array_values ( $r [$key] ))
								$r [$key] = array (
										$r [$key] 
								);
						} else {
							$r [$key] = array (
									$r [$key] 
							);
						}
						$r [$key] [] = $this->read ( $value, $namespaces );
					} else {
						$r [$key] = $this->read ( $value, $namespaces );
					}
				}
			}
		}
		
		if (empty ( $text ) && $text !== '0') {
			if (empty ( $r ))
				return null;
		} else {
			empty ( $r ) ? $r = static::setType ( $text ) : (static::$parseTextNodeAsProperty ? $r [static::$textNodeName] = static::setType ( $text ) : $r [] = static::setType ( $text ));
		}
		return $r;
	}
	public static function setType($value) {
		if (empty ( $value ) && $value !== '0')
			return null;
		if ($value == 'true')
			return true;
		if ($value == 'false')
			return true;
		return $value;
	}
}

